/*
 * Copyright (c) 2018. , apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.wsonrpc.server.support;

import net.apexes.wsonrpc.core.WebSocketSession;
import net.apexes.wsonrpc.core.WsonrpcConfig;
import net.apexes.wsonrpc.core.WsonrpcConfigBuilder;
import net.apexes.wsonrpc.server.PathAcceptor;
import net.apexes.wsonrpc.server.PathAcceptors;
import net.apexes.wsonrpc.server.WsonrpcCloseListener;
import net.apexes.wsonrpc.server.WsonrpcMessageListener;
import net.apexes.wsonrpc.server.WsonrpcOpenListener;
import net.apexes.wsonrpc.server.WsonrpcPingListener;
import net.apexes.wsonrpc.server.WsonrpcRequestInterceptor;
import net.apexes.wsonrpc.server.WsonrpcServer;
import net.apexes.wsonrpc.server.WsonrpcServerBase;
import org.java_websocket.WebSocket;
import org.java_websocket.WebSocketAdapter;
import org.java_websocket.WebSocketImpl;
import org.java_websocket.WebSocketListener;
import org.java_websocket.WebSocketServerFactory;
import org.java_websocket.drafts.Draft;
import org.java_websocket.exceptions.InvalidDataException;
import org.java_websocket.framing.CloseFrame;
import org.java_websocket.framing.Framedata;
import org.java_websocket.framing.PingFrame;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.handshake.ServerHandshakeBuilder;
import org.java_websocket.server.DefaultWebSocketServerFactory;
import org.java_websocket.server.WebSocketServer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 基于 {@link org.java_websocket.server.WebSocketServer}的服务端
 * 
 * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
 *
 */
public class JavaWebsocketWsonrpcServer {

    protected final WebSocketServer websocketServer;
    protected final WsonrpcServerBase serverBase;
    private final AtomicBoolean isclose = new AtomicBoolean(false);
    private WsonrpcPingListener pingListener;

    public JavaWebsocketWsonrpcServer(int port) {
        this(port, PathAcceptors.rootPath());
    }

    public JavaWebsocketWsonrpcServer(int port, WsonrpcConfig config) {
        this(port, PathAcceptors.rootPath(), config);
    }
    
    public JavaWebsocketWsonrpcServer(int port, PathAcceptor pathAcceptor) {
        this(port, pathAcceptor, WsonrpcConfigBuilder.defaultConfig());
    }

    public JavaWebsocketWsonrpcServer(int port, PathAcceptor pathAcceptor, WsonrpcConfig config) {
        websocketServer = new WebSocketServerAdapter(new InetSocketAddress(port), pathAcceptor, this);
        WebSocketServerFactory wsf = new DefaultWebSocketServerFactory() {

            @Override
            public WebSocketImpl createWebSocket(WebSocketAdapter adapter, Draft draft) {
                return new SessionWebSocketImpl(adapter, draft);
            }

            @Override
            public WebSocketImpl createWebSocket(WebSocketAdapter adapter, List<Draft> draft) {
                return new SessionWebSocketImpl(adapter, draft);
            }
        };
        websocketServer.setWebSocketFactory(wsf);
        serverBase = new WsonrpcServerBase(config);
    }

    public WsonrpcServer getWsonrpcServer() {
        return serverBase;
    }

    public WsonrpcPingListener getPingListener() {
        return pingListener;
    }

    public void setPingListener(WsonrpcPingListener pingListener) {
        this.pingListener = pingListener;
    }

    public void setWsonrpcOpenListener(WsonrpcOpenListener listener) {
        serverBase.setWsonrpcOpenListener(listener);
    }

    public void setWsonrpcCloseListener(WsonrpcCloseListener listener) {
        serverBase.setWsonrpcCloseListener(listener);
    }

    public void setWsonrpcMessageListener(WsonrpcMessageListener listener) {
        serverBase.setWsonrpcMessageListener(listener);
    }

    public void setWsonrpcRequestInterceptor(WsonrpcRequestInterceptor interceptor) {
        serverBase.setWsonrpcRequestInterceptor(interceptor);
    }

    public void start() {
        isclose.set(false);
        try {
            websocketServer.start();
        } finally {
            isclose.set(true);
        }
    }

    public void stop() throws IOException, InterruptedException {
        isclose.set(true);
        websocketServer.stop();
    }

    public boolean isRunning() {
        return !isclose.get();
    }

    private static String sessionId(WebSocket websocket) {
        if (websocket == null) {
            return null;
        }
        String id;
        if (websocket instanceof SessionWebSocketImpl) {
            id = ((SessionWebSocketImpl) websocket).getId();
        } else {
            id = websocket.getRemoteSocketAddress().toString();
        }
        return id;
    }

    /**
     * 
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     *
     */
    private static class SessionWebSocketImpl extends WebSocketImpl {

        private final String id = UUID.randomUUID().toString();

        public SessionWebSocketImpl(WebSocketListener listener, Draft draft) {
            super(listener, draft);
        }

        public SessionWebSocketImpl(WebSocketListener listener, List<Draft> drafts) {
            super(listener, drafts);
        }

        String getId() {
            return id;
        }

    }

    /**
     * 
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     *
     */
    private static class WebSocketServerAdapter extends WebSocketServer {

        private final JavaWebsocketWsonrpcServer server;
        private final PathAcceptor pathAcceptor;

        public WebSocketServerAdapter(InetSocketAddress address, PathAcceptor pathAcceptor,
                JavaWebsocketWsonrpcServer server) {
            super(address);
            this.pathAcceptor = pathAcceptor;
            this.server = server;
        }

        @Override
        public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket websockt, Draft draft,
                ClientHandshake request) throws InvalidDataException {
            if (pathAcceptor == null || pathAcceptor.accept(request.getResourceDescriptor())) {
                return super.onWebsocketHandshakeReceivedAsServer(websockt, draft, request);
            }
            throw new InvalidDataException(CloseFrame.TLS_ERROR, request.getResourceDescriptor());
        }

        @Override
        public void onOpen(WebSocket websocket, ClientHandshake handshake) {
            server.serverBase.onOpen(new JavaWebSocketSessionAdapter(websocket));
        }

        @Override
        public void onStart() {
        }

        @Override
        public void onClose(WebSocket websocket, int code, String reason, boolean remote) {
            server.serverBase.onClose(sessionId(websocket));
        }

        @Override
        public void onMessage(WebSocket websocket, String message) {
            server.serverBase.onMessage(sessionId(websocket), ByteBuffer.wrap(message.getBytes()));
        }

        @Override
        public void onMessage(WebSocket websocket, ByteBuffer message) {
            server.serverBase.onMessage(sessionId(websocket), message);
        }

        @Override
        public void onWebsocketPing(WebSocket websocket, Framedata framedata) {
            WsonrpcPingListener listener = server.getPingListener();
            if (listener != null) {
                listener.onPing(sessionId(websocket), framedata.getPayloadData().array());
            }
        }

        @Override
        public void onError(WebSocket websockt, Exception ex) {
            if (server.isRunning()) {
                server.serverBase.onError(sessionId(websockt), ex);
            }
        }
    }

    /**
     * 
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     *
     */
    private static class JavaWebSocketSessionAdapter implements WebSocketSession {

        private static final PingFrame PING_FRAME = new PingFrame();

        private final WebSocket websocket;

        JavaWebSocketSessionAdapter(WebSocket websocket) {
            this.websocket = websocket;
        }

        @Override
        public String getId() {
            return sessionId(websocket);
        }

        @Override
        public boolean isOpen() {
            return websocket.isOpen();
        }

        @Override
        public void sendBinary(byte[] bytes) {
            websocket.send(bytes);
        }

        @Override
        public void ping(byte[] bytes) throws IOException {
            PingFrame frame;
            if (bytes == null || bytes.length == 0) {
                frame = PING_FRAME;
            } else {
                frame = new PingFrame();
                frame.setPayload(ByteBuffer.wrap(bytes));
            }
            websocket.sendFrame(frame);
        }

        @Override
        public void close() throws IOException {
            websocket.close();
        }

    }

}
